portal Michała Hanćkowiaka
Begin main content
Sieci komputerowe (3)

Sieci komputerowe — ćwiczenia 3


Temat zajęć: Programowanie gniazd BSD c.d.

Literatura:
  • R. Stevens, "Programowanie zastosowań sieciowych w systemie Unix"
  • A. Jones, J. Ohlund, "Programowanie sieciowe Microsoft Windows"
  • C. Petzold, "Programowanie Windows"
  • M. Gabassi, B. Dupouy, "Przetwarzanie rozproszone w systemie Unix"
  • E. Harold, "Java: programowanie sieciowe"
  • R. Stevens, "Biblia TCP/IP" (tom 1 - Protokoły i tom 2 - Implementacje)
  • C. Hunt, "TCP/IP - administracja sieci"
  • V. Toth, "Programowanie Windows 98/NT - księga eksperta"
  • Dokumenty RFC wersja on-line

 Wykorzystanie funkcji select

Funkcja select umożliwia nadzorowanie zbioru deskryptorów pod względem możliwości odczytu, zapisu bądź wystąpienia sytuacji wyjątkowych. Formalnie prototyp funkcji wygląda następująco (definicja w sys/select.h):
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) 
Funkcja przyjmuje wspomniane trzy zbiory deskryptorów, jednak nie ma obowiązku określania ich wszystkich (można w miejsce odp. zbioru deskryptorów podać NULL - wówczas dany zbiór nie będzie nadzorowany przez select).
W celu umożliwienia nasłuchu na dwóch (lub więcej) gniazdach jednocześnie, należy postępować wg następującego schematu:
  1. wstaw deskryptory gniazd (g1 i g2) do zbioru readfds
  2. ustaw timeout
  3. wywołaj select na zbiorze readfds
  4. jeśli select zwrócił wartość dodatnią, to
    1. sprawdź czy g1 jest ustawiony w readfds, jeśli tak, to obsłuż odczyt na gnieździe g1
    2. sprawdź czy g2 jest ustawiony w readfds, jeśli tak, to obsłuż odczyt na gnieździe g2
  5. ew. powrót do 1
W przypadku gniazd TCP select zwróci gotowość deskryptora jeśli możliwe jest wywołanie na gnieździe funkcji accept (gniazdo nasłuchujące) lub recv (gniazdo komunikacyjne) bez blokowania aplikacji (czyli w momencie, w którym istnieje oczekujące połączenie na gnieździe nasłuchującym lub gdy czekają dane w buforze na gnieździe komunikacyjnym).
W przypadku gniazd UDP select zwróci gotowość deskryptora jeśli możliwe jest nieblokujące wywołanie recvfrom (w buforze odczytu oczekuje datagram).

Istotne informacje na temat select (dostępne w man pages):
  • do czyszczenia zbioru deskryptorów służy FD_ZERO(fd_set *fds)
  • do dodawania deskryptora do zbioru służy FD_SET(int fd, fd_set *fds)
  • do usuwania deskryptora ze zbioru służy FD_CLR(int fd, fd_set *fds)
  • do sprawdzania przynależności deskryptora do zbioru służy FD_ISSET(int fd, fd_set *fds)
  • funkcja select jako swój wynik zwraca liczbę "gotowych" deskryptorów
  • pierwszym parametrem select musi być największa wartość deskryptora ze zbiorów powiększona o 1, a NIE liczba deskryptorów (częsty błąd!)
  • jeśli jako timeout podamy NULL, select wraca natychmiast, informując jaki jest bieżący stan deskryptorów
  • jeśli jako timeout podamy niezerowy czas, select wraca po upływie tego czasu lub po wystąpieniu zdarzenia na deskryptorze (zależy co nastąpi wcześniej)
  • select może (zależnie od implementacji) zmienić wartość timeout, zatem należy zawsze ustawiać czas oczekiwania na nowo przed wywołaniem funkcji
  • jeśli jako timeout podamy zerowy czas (ale nie NULL), select wróci natychmiast (rodzaj pollingu), jeżeli natomiast wpiszemy w miejsce timeval NULL, to select wróci dopiero po wystąpieniu zdarzenia (innymi słowy zerowy czas oczekiwania oznacza czekanie w nieskończoność na wystąpienie zdarzenia)
  • struktura timeval posiada pola tv_sec (sekundy) i tv_usec (mikrosekundy)

 Przykład 1

Przykład zawiera implementację serwera, który nasłuchuje zarówno na TCP, jak i UDP. Program ten może służyć jako serwer zarówno dla klienta z Przykładu 5 z Ćwiczeń 1, jak i dla klienta z Przykładu 1 z Ćwiczeń 2 (proszę sprawdzić oba, uruchamiając je jednocześnie dla jednej instancji poniższego serwera).

Plik c3p1.c  pobierz
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>

void DrukujNadawce(struct sockaddr_in *adres)
{
printf("Wiadomosc od %s:%d",
inet_ntoa(adres->sin_addr),
ntohs(adres->sin_port)
);
}

void ObsluzTCP(int gniazdo, struct sockaddr_in *adres)
{
int nowe_gniazdo;
char bufor[1024];
socklen_t dladr = sizeof(struct sockaddr_in);
nowe_gniazdo =
accept(gniazdo, (struct sockaddr*) adres,
&dladr);
if (nowe_gniazdo < 0)
{
printf("Bledne polaczenie (accept < 0)\n");
return;
}
memset(bufor, 0, 1024);
while (recv(nowe_gniazdo, bufor, 1024, 0) <= 0);
DrukujNadawce(adres);
printf("[TCP]: %s\n", bufor);
close(nowe_gniazdo);
}

void ObsluzUDP(int gniazdo, struct sockaddr_in *adres)
{
char bufor[1024];
socklen_t dladr = sizeof(struct sockaddr_in);
memset(bufor, 0, 1024);
recvfrom(gniazdo, bufor, 1024, 0, (struct sockaddr*) adres,
&dladr);
DrukujNadawce(adres);
printf("[UDP]: %s\n", bufor);
}

void ObsluzObaProtokoly(int gniazdoTCP, int gniazdoUDP,
struct sockaddr_in *adres)
{
fd_set readfds;
struct timeval timeout;
unsigned long proba;
int maxgniazdo;

maxgniazdo = (gniazdoTCP > gniazdoUDP ?
gniazdoTCP+1 : gniazdoUDP+1);
proba = 0;

while(1)
{
FD_ZERO(&readfds);
FD_SET(gniazdoTCP, &readfds);
FD_SET(gniazdoUDP, &readfds);
timeout.tv_sec = 1;
timeout.tv_usec = 0;
if (select(maxgniazdo, &readfds, NULL, NULL, &timeout) > 0)
{
proba = 0;
if (FD_ISSET(gniazdoTCP, &readfds))
ObsluzTCP(gniazdoTCP, adres);
if (FD_ISSET(gniazdoUDP, &readfds))
ObsluzUDP(gniazdoUDP, adres);
}
else
{
proba++;
printf("Czekam %lu sekund i nic ...\n", proba);
}
}
}

int main(void)
{
struct sockaddr_in bind_me_here;
int gt, gu, port;

printf("Numer portu: ");
scanf("%d", &port);

gt = socket(PF_INET, SOCK_STREAM, 0);
gu = socket(PF_INET, SOCK_DGRAM, 0);

bind_me_here.sin_family = AF_INET;
bind_me_here.sin_port = htons(port);
bind_me_here.sin_addr.s_addr = INADDR_ANY;

if (bind(gt,(struct sockaddr*) &bind_me_here,
sizeof(struct sockaddr_in)) < 0)
{
printf("Bind na TCP nie powiodl sie.\n");
return 1;
}

if (bind(gu,(struct sockaddr*) &bind_me_here,
sizeof(struct sockaddr_in)) < 0)
{
printf("Bind na UDP nie powiodl sie.\n");
return 1;
}

listen(gt, 10);

ObsluzObaProtokoly(gt, gu, &bind_me_here);

return 0;
}

 Serwery równoległe

Już na pierwszych zajęciach omawialiśmy problem obsługi wielu łączących się klientów w tym samym czasie. Wiemy, że dla protokołu TCP funkcja listen umożliwia ustalenie wielkości kolejki oczekujących połączeń. Zatem w czasie, gdy usługa (serwer) komunikuje się z jednym klientem, inni klienci próbujący połączyć się z gniazdem usługi zostają umieszczeni w kolejce. Jeśli jednak natura usługi wymaga długotrwałej komunikacji z jednym klientem, może to doprowadzić do niedostępności usługi dla innych klientów przez dłuższy czas. Istnieje proste rozwiązanie tego problemu, które opiera się na właściwości gniazd TCP. Otóż w momencie, gdy funkcja accept zwróci deskryptor nowego gniazda (jakiś klient wykonał connect), można zrównoleglić program usługi (np. wykonując fork) i w procesie potomnym obsłużyć właśnie połączonego klienta, zaś w procesie macierzystym powrócić do oczekiwania na kolejne połączenia (czyli ponownie wykonać accept). Scenariusz taki możliwy jest tylko w przypadku protokołu TCP (nie UDP), ponieważ protokół ten zapewnia wzajemnie jednoznaczne skojarzenie gniazd dla pary connect, accept. Innymi słowy, system po stronie usługi jest w stanie rozróżnić, czy przychodzący segment TCP dotyczy już toczącej się "rozmowy", czy jest to początek nowego połączenia (nowy connect od innego klienta).


 Przykład 2

Załóżmy, że implementujemy usługę (i jej klientów), której zadaniem jest przesyłanie na żądanie zawartości pliku o podanej przez klienta ścieżce. Klient łączy się na ustalony port usługi (np. 21212) i przesyła ścieżkę do pliku (w systemie plików maszyny, na której działa serwer usługi), którego zawartość chce pobrać. Usługa odsyła najpierw wielkość pliku (long w formacie sieci), a następnie samą zawartość pliku. Zauważmy, że przesłanie pliku może zająć dłuższy czas (w zależności od rozmiaru pliku i dostępnej przepustowości łącza), a dodatkowe opóźnienia może wprowadzać sama konstrukcja kodu klienta. Jeśli bowiem po połączeniu program kliencki będzie czekał, aż użytkownik poda ścieżkę do pliku, to może on blokować usługę przez nieokreślony z góry czas. Zatem aby umożliwić innym klientom korzystanie z usługi, po nawiązaniu połączenia przez klienta serwer utworzy swój proces potomny, który zajmie się "rozdawaniem" plików konkretnemu klientowi, podczas gdy proces macierzysty powróci do accept i będzie czekał na kolejnych klientów.

Plik c3p2a.c  pobierz
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <sys/types.h>
#include <sys/stat.h>

#define PORT htons(21212)

void ObsluzPolaczenie(int gn)
{
char sciezka[512];
long dl_pliku, wyslano, wyslano_razem, przeczytano;
struct stat fileinfo;
FILE* plik;
unsigned char bufor[1024];

memset(sciezka, 0, 512);
if (recv(gn, sciezka, 512, 0) <= 0)
{
printf("Potomny: blad przy odczycie sciezki\n");
return;
}

printf("Potomny: klient chce plik %s\n", sciezka);

if (stat(sciezka, &fileinfo) < 0)
{
printf("Potomny: nie moge pobrac informacji o pliku\n");
return;
}

if (fileinfo.st_size == 0)
{
printf("Potomny: rozmiar pliku 0\n");
return;
}

printf("Potomny: dlugosc pliku: %d\n", fileinfo.st_size);

dl_pliku = htonl((long) fileinfo.st_size);

if (send(gn, &dl_pliku, sizeof(long), 0) != sizeof(long))
{
printf("Potomny: blad przy wysylaniu wielkosci pliku\n");
return;
}

dl_pliku = fileinfo.st_size;
wyslano_razem = 0;
plik = fopen(sciezka, "rb");
if (plik == NULL)
{
printf("Potomny: blad przy otwarciu pliku\n");
return;
}

while (wyslano_razem < dl_pliku)
{
przeczytano = fread(bufor, 1, 1024, plik);
wyslano = send(gn, bufor, przeczytano, 0);
if (przeczytano != wyslano)
break;
wyslano_razem += wyslano;
printf("Potomny: wyslano %d bajtow\n", wyslano_razem);
}

if (wyslano_razem == dl_pliku)
printf("Potomny: plik wyslany poprawnie\n");
else
printf("Potomny: blad przy wysylaniu pliku\n");
fclose(plik);
return;
}


int main(void)
{
int gn_nasluch, gn_klienta;
struct sockaddr_in adr;
socklen_t dladr = sizeof(struct sockaddr_in);

gn_nasluch = socket(PF_INET, SOCK_STREAM, 0);
adr.sin_family = AF_INET;
adr.sin_port = PORT;
adr.sin_addr.s_addr = INADDR_ANY;
memset(adr.sin_zero, 0, sizeof(adr.sin_zero));

if (bind(gn_nasluch, (struct sockaddr*) &adr, dladr) < 0)
{
printf("Glowny: bind nie powiodl sie\n");
return 1;
}

listen(gn_nasluch, 10);

while(1)
{
dladr = sizeof(struct sockaddr_in);
gn_klienta = accept(gn_nasluch, (struct sockaddr*) &adr, &dladr);
if (gn_klienta < 0)
{
printf("Glowny: accept zwrocil blad\n");
continue;
}
printf("Glowny: polaczenie od %s:%u\n",
inet_ntoa(adr.sin_addr),
ntohs(adr.sin_port)
);
printf("Glowny: tworze proces potomny\n");
if (fork() == 0)
{
/* proces potomny */
printf("Potomny: zaczynam obsluge\n");
ObsluzPolaczenie(gn_klienta);
printf("Potomny: zamykam gniazdo\n");
close(gn_klienta);
printf("Potomny: koncze proces\n");
exit(0);
}
else
{
/* proces macierzysty */
printf("Glowny: wracam do nasluchu\n");
continue;
}
}
return 0;
}

Plik c3p2b.c  pobierz
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <sys/types.h>
#include <netdb.h>

#define IP(H) *((unsigned long*) (H)->h_addr_list[0])

int main(void)
{
int gn;
struct sockaddr_in adr;
int port;
struct hostent *h;
char nazwa[512];
char bufor[1025];
char sciezka[512];
long dl_pliku, odebrano, odebrano_razem;

printf("Nazwa hosta / adres IP: ");
scanf("%s", nazwa);
h = gethostbyname(nazwa);
if (h == NULL)
{
printf("Nieznany host\n");
return 1;
}
printf("Numer portu: ");
scanf("%d", &port);
adr.sin_family = AF_INET;
adr.sin_port = htons(port);
adr.sin_addr.s_addr = IP(h);

printf("Lacze sie z %s:%d\n",
inet_ntoa(adr.sin_addr),
port);

gn = socket(PF_INET, SOCK_STREAM, 0);
if (connect(gn, (struct sockaddr*) &adr, sizeof(adr))<0)
{
printf("Nawiazanie polaczenia nie powiodlo sie\n");
close(gn);
return 1;
}
printf("Polaczenie nawiazane\n");
printf("Podaj sciezke do pliku: \n");
memset(sciezka, 0, 512);
scanf("%s",sciezka);
printf("Wysylam sciezke\n");
if (send(gn, sciezka, strlen(sciezka), 0) != strlen(sciezka))
{
printf("Blad przy wysylaniu sciezki\n");
close(gn);
return 1;
}
printf("Sciezka wyslana. Odczytuje dlugosc pliku.\n");
if (recv(gn, &dl_pliku, sizeof(long), 0) != sizeof(long))
{
printf("Blad przy odbieraniu dlugosci\n");
printf("Moze plik nie istnieje?\n");
close(gn);
return 1;
}
dl_pliku = ntohl(dl_pliku);
printf("Plik ma dlugosc %d\n", dl_pliku);
printf("----- ZAWARTOSC PLIKU -----\n");
odebrano_razem = 0;
while (odebrano_razem < dl_pliku)
{
memset(bufor, 0, 1025);
odebrano = recv(gn, bufor, 1024, 0);
if (odebrano < 0)
break;
odebrano_razem += odebrano;
fputs(bufor, stdout);
}
close(gn);
if (odebrano_razem != dl_pliku)
printf("*** BLAD W ODBIORZE PLIKU ***\n");
else
printf("*** PLIK ODEBRANY POPRAWNIE ***\n");
return 0;
}

Zadanie 1

Możliwość zrównoleglania obsługi wielu klientów jest niewątpliwą zaletą w kontekście programowania gniazd. Jednak tworzenie  wielu procesów potomnych bez żadnej kontroli ich liczby jest niedopuszczalne z punktu widzenia bezpieczeństwa systemu. Dlatego w większości przypadków usług sieciowych określa się maksymalną liczbę klientów, jacy mogą być obsługiwani przez usługę w tym samym czasie (innymi słowy maksymalną liczbę procesów potomnych usługi). Proszę zmodyfikować kod z przykładu 2 tak, aby usługa obsługiwała nie więcej niż 10 klientów jednocześnie.
Podpowiedź: należy wprowadzić licznik procesów potomnych i wykorzystać funkcję systemową wait (proszę sprawdzić man 2 wait).

 Zadanie (domowe) — powtórka BSD

W czasie następnych zajęć (ćwiczenia 4) za tydzień, na hoście o adresie 150.254.77.129 na porcie podanym na tablicy (w przykładach 4444), nasłuchiwać będzie pewien program — serwer. Należy zaimplementować program kliencki, który zrealizuje następujący protokół:

Protokół dla Zadania

Proszę zaimplementować do niego program kliencki. Program powinien zakodować numer indeksu autora jako long w formacie sieci i wysłać pod wskazany adres i numer portu. Następnie powinien odebrać pewną liczbę (również jako long w formacie sieci), zdekodować ją do formatu hosta, dodać do niej 1, ponownie zakodować do formatu sieci i odesłać na: 150.254.77.129:port. Wszystko musi odbywać się w jednej sesji (za pomocą jednego połączonego gniazda). Numery indeksów wszystkich studentów, których programy poprawnie zrealizują ten protokół, zostają zapisane w zbiorze, który zweryfikuję na końcu zajęć. W celu wyjaśnienia ew. wątpliwości, oraz aby umożliwić testowanie swoich programów klienckich we własnym zakresie, poniżej podaję kod nasłuchującego programu.

Plik c1za.c pobierz
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>

struct sockaddr_in endpoint;
FILE *flog;
char myhostname[1024];

int main(int argc, char **argv) {
long lnIn1, lnIn2, lhIn1, lhIn2, lhOut, lnOut;
int sdServerSocket, sdConnection, retval;
socklen_t sin_size;
struct sockaddr_in incoming;
struct hostent *heLocalHost;
char sign;

sin_size = sizeof(struct sockaddr_in);

sdServerSocket = socket(PF_INET, SOCK_STREAM, 0);
gethostname(myhostname, 1023);
heLocalHost = gethostbyname(myhostname);

endpoint.sin_family = AF_INET;
endpoint.sin_port = htons(14444);
endpoint.sin_addr = *(struct in_addr*)
heLocalHost->h_addr;
memset(&(endpoint.sin_zero),0,8);

printf("slucham na %s:%d\n",
inet_ntoa(endpoint.sin_addr),
ntohs(endpoint.sin_port));

retval = bind(sdServerSocket,
(struct sockaddr*) &endpoint,
sizeof(struct sockaddr));

if (retval < 0) {
printf("bind nie powiodl sie\n");
return 1;
}

listen(sdServerSocket, 10);

sin_size = sizeof(struct sockaddr_in);
while ((sdConnection =

accept(sdServerSocket,
(struct sockaddr*) &incoming,
&sin_size))

> 0) {
printf("Polaczenie z %s:%d\n",
inet_ntoa(incoming.sin_addr),
ntohs(incoming.sin_port));

if (recv(sdConnection, &lnIn1, sizeof(long),0)
!= sizeof(long)) {
printf("pierwszy recv nie powiodl sie\n");
close(sdConnection);
continue;
}
lhIn1 = ntohl(lnIn1);

lhOut = random();
lnOut = htonl(lhOut);

if (send(sdConnection, &lnOut, sizeof(long), 0)
!= sizeof(long)) {
printf("send nie powiodl sie\n");
close(sdConnection);
continue;
}

if (recv(sdConnection, &lnIn2, sizeof(long), 0)
!= sizeof(long)) {
printf("drugi recv nie powiodl sie\n");
close(sdConnection);
continue;
}

lhIn2 = ntohl(lnIn2);

flog = fopen("zad.txt","a");
if (lhIn2 == lhOut + 1) sign = '+';
else sign = '-';
fprintf(flog,"%c %ld from %s:%d : %ld, %ld\n",
sign,
lhIn1,
inet_ntoa(incoming.sin_addr),
ntohs(incoming.sin_port),
lhOut,
lhIn2);

close(sdConnection);
fflush(flog);
fclose(flog);
}

printf("Blad sieci\n");
fclose(flog);
return 0;
}

Zadanie do przemyślenia (na ćwiczenia 4)

Napisz serwer (pojedynczy proces z jednym wątkiem) wykorzystujący funkcję select(), który:
  1. nasłuchuje na porcie TCP 12346,
  2. rejestruje się (jako działający serwer) poprzez wysłanie pakietu UDP zawierającego dwucyfrowy numer grupy z USOS (11 — 1CA, 12 — 1CB, 13 — 1CC), spację, numer indeksu i znak nowego wiersza (np. "11 321321\n") na port 12346 serwera 150.254.77.101,
  3. po połączeniu klienta poprzez TCP przesyła wszystkie otrzymane od niego dane w paczkach po 100 bajtów przez UDP na port 12346, serwer 150.254.77.101 (jeżeli nie ma pełnych 100 bajtów, to czeka aż się uzbierają),
  4. jednocześnie przesyła wszystkie otrzymane przez UDP dane przez nawiązane wcześniej połączenie TCP,
  5. po 30 sekundach program kończy działanie.
Zadanie będzie wykonane poprawnie, jeżeli wszystkie dane zostaną przekopiowane poprawnie. Serwer potwierdzi poprawne wykonanie zadania pakietem UDP zawierającym "ok\n" (nie trzeba go kopiować). Uwaga: na jednym komputerze można uruchomić tylko jeden (jednego studenta) serwer, czas pomiędzy poszczególnymi uruchomieniami powinien wynosić co najmniej 30 sekund (jeżeli połączenie TCP zostało poprawnie zamknięte, w przeciwnym razie bezpieczniej kilka minut).



Valid HTML 4.01!

uwaga: portal używa ciasteczek tylko do obsługi tzw. sesji...